筆記目錄

Skip to content

在 .NET 使用 Server-Sent-Events(SSE)

TLDR

  • SSE 是一種單向連線技術,由 Server 主動向 Client 推送資料,適合減少頻寬浪費與伺服器負擔。
  • SSE 訊息格式必須以 data: <內容>\n\n 結尾,其中兩個換行符號代表一則訊息的結束。
  • Server 端必須將 Content-Type 設定為 text/event-stream 並禁用快取。
  • 若需處理跨域請求,需在 EventSource 設定 withCredentials: true,並確保 Server 端已正確設定 CORS。
  • 在 ASP.NET Core 中,應使用 Response.WriteAsync 配合 Response.Body.FlushAsync 來持續推送資料。

SSE 與其他即時通訊方式的比較

在開發即時通訊功能時,常見的技術選擇包含 Polling、SSE 與 WebSocket,其特性差異如下:

  • Polling:Client 定期發送請求,容易導致頻繁的請求與回應,佔用過多伺服器資源。
  • Server-Sent-Events (SSE):建立單向連線,由 Server 向 Client 推送更新。優點在於僅需單一 TCP 連線,能有效降低伺服器負擔並減少頻寬浪費。
  • WebSocket:建立雙向連線,雙方可隨時傳送資料,適合需要頻繁雙向互動的場景。

SSE JavaScript 的實作

SSE 透過 EventSource 物件進行連線,並內建 openerrormessage 事件。

SSE 程式碼範例

什麼情況下會遇到這個問題:當需要從前端監聽來自 Server 的即時資料流時。

javascript
const sse = new EventSource('Your API Url');

// 監聽 open 事件,當連接成功時會觸發此事件
sse.addEventListener('open', function (e) {
  console.log('SSE connection opened');
});

// 監聽 error 事件,當連接錯誤時會觸發此事件
sse.addEventListener('error', function (e) {
  console.log('SSE connection error');
});

// 監聽 message 事件,當接收到訊息時會觸發此事件
sse.addEventListener('message', function (e) {
  console.log('SSE message received', e);

  const data = JSON.parse(e.data);
  const messageElement = document.createElement('div');
  messageElement.textContent = data.message;
  document.body.appendChild(messageElement);
});

// 監聽自訂的 end 事件
sse.addEventListener('end', function (e) {
  console.log('SSE custom end', e);

  sse.close();
});

withCredentials 屬性

什麼情況下會遇到這個問題:當 SSE 請求需要跨域並攜帶驗證資訊(如 Cookie)時。

在建立 EventSource 物件時,可使用 withCredentials 屬性。若為跨域請求,Server 端必須同步設定 CORS 標頭以允許驗證資訊。

javascript
const sse = new EventSource('Your API Url', { withCredentials: true } );

WARNING

SSE 使用跨域請求時,Server 端的 Header 也需要進行相應的 CORS 設定。

在 .NET 中實作 SSE Server

SSE Server 必須回傳 text/event-stream 格式,且每條訊息以空行(\n\n)作為結束標記。

以 ASHX (泛型處理常式) 實作

什麼情況下會遇到這個問題:在傳統 ASP.NET Web Forms 專案中需要實現輕量級的即時推送。

csharp
public class SseHandler : IHttpHandler {
    public void ProcessRequest (HttpContext context) {
        // 設定 response 的 Content-Type 為 text/event-stream
        context.Response.ContentType = "text/event-stream";
        context.Response.CacheControl = "no-cache";

        // 模擬一個 SSE 事件流,每秒發送一個訊息
        int count = 0;
        while (count < 10) {
            count++;
            context.Response.Write("data: " + "{\"message\": \"Hello SSE " + count + "\"}\n\n");
            context.Response.Flush();
            System.Threading.Thread.Sleep(1000);
        }

        // 回傳自訂的 end 事件,前面 JavaScript 範例會在此 Close SSE
        context.Response.Write("event: end\ndata: {}\n\n");
        context.Response.Flush();
        context.Response.End();
    }

    public bool IsReusable {
        get {
            return false;
        }
    }
}

TIP

每條訊息以空行作結束,所以第一個 \n 表示訊息換行,第二個 \n 以示訊息結束。

以 ASP.NET Core Web API 實作

什麼情況下會遇到這個問題:在現代 ASP.NET Core 架構下,需要透過 Controller 處理即時資料推送。

csharp
[ApiController]
[Route("[controller]")]
public class SseController : ControllerBase {
    [HttpGet]
    public async Task GetAsync() {
        // 設定 response 的 Content-Type 為 text/event-stream
        Response.Headers.Add("Content-Type", "text/event-stream;");
        Response.Headers.Add("Cache-Control", "no-cache");

        // 模擬一個 SSE 事件流,每秒發送一個訊息
        int count = 0;
        while (count < 10) {
            count++;
            await Response.WriteAsync($"data: " + "{\"message\": \"Hello SSE " + count + "\"}\n\n");
            await Response.Body.FlushAsync();
            await Task.Delay(TimeSpan.FromSeconds(1));
        }

        // 回傳自訂的 end 事件,前面 JavaScript 範例會在此 Close SSE
        await Response.WriteAsync("event: end\ndata: {}\n\n");
        await Response.Body.FlushAsync();
    }
}

異動歷程

  • 2023-03-15 初版文件建立。
  • 2024-02-17 修正 withCredentials 的相關內容。